package com.hontech.bread.device

import com.hontech.bread.R
import com.hontech.bread.app.AppContext
import com.hontech.bread.device.mdb.Bill
import com.hontech.bread.device.mdb.Coin
import com.hontech.bread.device.mdb.MdbFrame
import com.hontech.bread.device.mdb.Pos
import com.hontech.bread.utils.ConnTask
import com.hontech.bread.utils.ExecException
import com.hontech.bread.utils.TimeoutException
import com.hontech.bread.utils.await
import com.hontech.bread.utils.log
import com.hontech.bread.utils.runIO
import com.hontech.bread.utils.stringById
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import serial.SerialPort
import serial.codec.CodecType
import serial.codec.u8
import serial.usb.PackageException
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap

object Conn {

    private const val BAUD_RATE = 115200
    private var globalSeq = 0
    private var serialPort: SerialPort? = null
    private val respMap = ConcurrentHashMap<Int, CompletableDeferred<Frame>>()
    private val serialMutex = Mutex(false)

    suspend fun autoOpen() {

        open(AppContext.resources.getStringArray(R.array.serial_ports)[0])
    }

    suspend fun open(name: String) = serialMutex.withLock {
        val sp = SerialPort.open(AppContext, name, BAUD_RATE)
        ReadTask(sp, respMap).start()
        serialPort = sp
    }

    fun isOpen() = serialPort != null

    suspend fun close() = serialMutex.withLock {
        serialPort?.close()
        serialPort = null
    }

    private fun getSeq(): Int {
        val seq = globalSeq
        globalSeq += 1
        if (globalSeq > 255) {
            globalSeq = 0
        }
        return seq
    }

    suspend fun ping() {
        val seq = getSeq()
        val buf = Frame.make(Type.PING, seq)
        exec(Type.PONG, seq, buf, stringById(R.string.err_pong_timeout))
    }

    suspend fun simpleReq(
        cmd: Int,
        args: Array<CodecType>,
        result: Array<CodecType>,
        timeout: Long = 500
    ) {
        val seq = getSeq()
        val buf = Frame.make(Type.SIMPLE_REQ, seq, cmd.u8(), *args)
        val frame = exec(Type.SIMPLE_RES, seq, buf, stringById(R.string.err_resp_timeout), timeout)
        frame.parse(*result)
    }

    suspend fun req(cmd: Int, args: Array<CodecType>, result: Array<CodecType>) {
        val seq = getSeq()

        val promise = CompletableDeferred<Frame>()
        respMap[(Type.RES shl 8) + seq] = promise

        exec(Type.ACK, seq, Frame.makeSession(seq), stringById(R.string.err_ack_timeout))

        exec(
            Type.ACK,
            seq,
            Frame.make(Type.REQ, seq, cmd.u8(), *args),
            stringById(R.string.err_ack_timeout)
        )

        while (true) {

            try {
                val frame = promise.await(3000, stringById(R.string.err_resp_timeout))
                frame.parse(*result)
                return
            } catch (_: TimeoutException) {
                log("res ${stringById(R.string.t1733460848_29)} ${stringById(R.string.t1733460848_30)}")
                try {
                    queryTask(cmd, seq)
                } catch (e: ExecException) {
                    val frame = promise.await(3000, stringById(R.string.err_resp_timeout))
                    frame.parse(*result)
                    return
                }
            }
        }
    }

    suspend fun mdbReq(cmd: Int, vararg args: CodecType): MdbFrame {
        val seq = getSeq()
        val id = (Type.MDB_RES shl 8) + seq
        val promise = CompletableDeferred<Frame>()
        respMap[id] = promise
        val buf = Frame.make(Type.MDB_REQ, seq, cmd.u8(), *args)
        write(buf)
        val frame = promise.await(800, stringById(R.string.err_resp_timeout))
        return frame.toMdb()
    }

    private suspend fun queryTask(cmd: Int, seq: Int) {
        simpleReq(Cmd.QUERY_TASK, arrayOf(cmd.u8(), seq.u8()), arrayOf())
    }

    private suspend fun exec(
        rType: Int,
        seq: Int,
        buf: ByteArray,
        msg: String,
        timeout: Long = 500
    ): Frame {
        val id = (rType shl 8) + seq

        for (i in 0 until 3) {

            try {
                val promise = CompletableDeferred<Frame>()
                respMap[id] = promise
                write(buf)
                return promise.await(timeout, msg)
            } catch (e: TimeoutException) {
                e.printStackTrace()

                if (i == 2) {
                    throw e
                }
            }
        }
        throw RuntimeException("no exec")
    }

    private suspend fun write(buf: ByteArray) = serialMutex.withLock {
        runIO {
            serialPort?.write(buf) ?: throw IOException(stringById(R.string.err_sp))
        }
    }
}

private class ReadTask(
    private val serialPort: SerialPort,
    private val respMap: Map<Int, CompletableDeferred<Frame>>
) : Thread("Read task") {

    override fun run() {

        while (true) {

            try {
                val buf = serialPort.read()
                val frame = Frame(buf)
                dispatch(frame)
            } catch (e: PackageException) {
                e.printStackTrace()
            } catch (e: ExecException) {
                e.printStackTrace()
            } catch (e: Exception) {
                e.printStackTrace()
                break
            }
        }

        log("read task exit")
    }


    private fun dispatch(frame: Frame) {

        when (frame.type) {

            Type.PING -> {
                ConnTask.post { serialPort.write(Frame.makePong(frame.seq)) }
            }

            Type.RES -> {
                ConnTask.post { serialPort.write(Frame.makeAck(frame.seq)) }
                respMap[frame.id()]?.complete(frame)
            }

            Type.PONG, Type.ACK, Type.SIMPLE_RES, Type.MDB_RES -> {
                respMap[frame.id()]?.complete(frame)
            }

            Type.NOTIFY_ACK -> {
                ConnTask.post { serialPort.write(Frame.makeAck(frame.seq)) }
                onNotify(frame)
            }

            Type.NOTIFY -> {
                onNotify(frame)
            }

            Type.MDB_COIN_NOTIFY -> Coin.onPollNotify(frame.toMdb())
            Type.MDB_BILL_NOTIFY -> Bill.onPollNotify(frame.toMdb())
            Type.MDB_POS_NOTIFY -> Pos.onPollNotify(frame.toMdb())
        }
    }
}



